package hxm.core;

import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;

import hxm.core.MExpr;
import hxm.core.Resolver;
import hxm.monads.OptionMonad;


#if macro
using tink.macro.tools.ExprTools;
#end

/**
 * The typer's job is to augment a MExpr with a type info, which is then labeled TMExpr. The type
 * refers to the monadic type of the current block, not the haxe types (it lets the compiler take
 * care of that as usual).
 * 
 * @author Simon Krajewski
 */
class Typer 
{
	#if macro
	
	static public function typeMExpr(mexpr:MExpr, ctx:MonadicContext, alsoTypeExpr:Bool = false):TMExpr
	{
		var expr:TMExpr = switch(mexpr.expr)
		{
			case MCall(e, p, i):
				var v = [];
				for (param in p) v.push(typeMExpr(param, ctx));
				{ type: ctx.type, pos:mexpr.pos, expr:MCall(typeMExpr(e, ctx), v, typeMExpr(i, ctx)) };
			case MType(t):
				// We type the expression and unify it with our context.
				var t = typeMExpr(t, ctx, true);
				ctx.type = Some(unify(t.type, ctx.type, t.pos));
				{ type: t.type, pos:t.pos, expr:MExpr(EBlock([]).at()) };
			case MSub(b):
				// We type the expression into a new context, but the sub's type is of the current context.
				var bType = typeMExpr(b, MacroHelper.cloneMonadicContext(ctx, None) );
				{ type:ctx.type, pos:bType.pos, expr:MSub(bType) };
			case MReturn(v):
				var ret = v == null ? MReturn() : MReturn(typeMExpr(v, ctx));
				switch(ctx.type)
				{
					case None:
						Context.warning("[Typer] Reached return without knowing monadic context.", mexpr.pos);
						{ type:Some(MacroHelper.getNeutralType()), pos:mexpr.pos, expr:ret };
					case Some(typeInfo):
						{ type:ctx.type, pos:mexpr.pos, expr:ret };						
				}

			case MExpr(e):
				// We type Expr only for MType expressions and the bind part of MBind. This is due the fact that
				// Context.typeof causes completion issues otherwise.
				if (alsoTypeExpr)
					{ type:MacroHelper.getTypeInfo(typeExpr(e, ctx)), pos:e.pos, expr:MExpr(e) };
				else
					{ type:ctx.type, pos:e.pos, expr:MExpr(e) };
			case MBlock(b):
				// A block's type is generally the type of its last expression.
				var types = [];
				for (e in b)
					types.push(typeMExpr(e, ctx));
				var type = types.length > 0
					? types[types.length - 1].type
					: MacroHelper.getTypeInfo(Context.getType("Dynamic"));
				{ type: type, pos:mexpr.pos, expr:MBlock(types) };
			case MBind(ident, bind, inner):
				// First we obtain the type of the bind expression and unify it with out context's type.
				var bindType = typeMExpr(bind, ctx, true);
				var unified = unify(bindType.type, ctx.type, bindType.pos);
				ctx.type = Some(unified);
				bindType.type = Some(unified);
				
				// If our type has a subtype, we store it as type of the identifier. This is needed to correctly infer
				// the type of the identifier in inner scopes.
				// TODO: What to do when there is no subtype?
				var identName = MacroHelper.getIdent(ident);
				switch(unified.subtype)
				{
					case None:
						ctx.idents.set(identName, Context.parse("null", Context.currentPos()));
					case Some(subTypeInfo):
						ctx.idents.set(identName, subTypeInfo.neutral);
				}
				
				// We can now type the inner context and unify its result type with our original context.
				var innerType = typeMExpr(inner, MacroHelper.cloneMonadicContext(ctx, Some(unified)));				
				unified = unify(innerType.type, Some(unified), innerType.pos);
				innerType.type = Some(unified);
				ctx.type = Some(unified);
				
				// The identifier must be of the unified type.
				var identType = { type:Some(unified), pos:ident.pos, expr:MExpr(EConst(CIdent(identName)).at(ident.pos)) };
				
				var bind = MBind(identType, bindType, innerType);
				{type:Some(unified), pos:mexpr.pos, expr:bind};
		}
		
		return expr;
	}
	
	static function typeExpr(expr:Expr, ctx:MonadicContext):Type
	{
		try
		{
			return Context.typeof(substitute(expr, ctx));
		}
		catch (err:haxe.macro.Error)
		{
			return Context.error("[Typer] Could not type expression: " +err + " | " +expr, expr.pos);
		}
	}
	
	static function substitute(expr:Expr, ctx:MonadicContext):Expr
	{
		switch(expr.expr)
		{
			case EConst(c):
				switch(c)
				{
					case CIdent(i):
						if (ctx.idents.exists(i))
							return ctx.idents.get(i);
						return expr;
					default:
						return expr;
				}
			case EArrayDecl(values):
				return EArrayDecl(substituteBlock(values, ctx)).at(expr.pos);
			case ECall(e, params):
				return ECall(substitute(e, ctx), substituteBlock(params, ctx)).at(expr.pos);
			case EBinop(op, e1, e2):
				return EBinop(op, substitute(e1, ctx), substitute(e2, ctx)).at(expr.pos);
			case EParenthesis(e):
				return EParenthesis(substitute(e, ctx)).at(expr.pos);
			case ETernary(c, e1, e2):
				return ETernary(substitute(c, ctx), substitute(e1, ctx), substitute(e2, ctx)).at(expr.pos);
			case EField(e, f):
				return EField(substitute(e, ctx), f).at(expr.pos);
			case EObjectDecl(e):
				var v = [];
				for (i in e)
					v.push( { field:i.field, expr:substitute(i.expr, ctx)});
				return EObjectDecl(v).at(expr.pos);
			case ENew(t, params):
				return ENew(t, substituteBlock(params, ctx)).at(expr.pos);
			case EWhile(econd, e, normalWhile):
				return EWhile(substitute(econd, ctx), substitute(e, ctx), normalWhile).at(expr.pos);
			case EUnop(op, postFix, e):
				return EUnop(op, postFix, substitute(e, ctx)).at(expr.pos);
				
			case EReturn(e):
				if (e == null) return EReturn().at(expr.pos);
				else return EReturn(substitute(e, ctx)).at(expr.pos);
			case EIf(econd, eif, eelse):
				if (eelse == null) return EIf(substitute(econd, ctx), substitute(eif, ctx),null).at(expr.pos);
				else return EIf(substitute(econd, ctx), substitute(eif, ctx), substitute(eelse, ctx)).at(expr.pos);
			case EFor(it, expr):
				return EFor(substitute(it, ctx), substitute(expr, ctx)).at(expr.pos);
			case EVars(vars):
				var ret = [];
				for (v in vars) ret.push( { name:v.name, type:v.type, expr:substitute(v.expr, ctx) } );
				return EVars(ret).at(expr.pos);
			case EUntyped(expr):
				return EUntyped(substitute(expr, ctx)).at(expr.pos);
			case EType(e, f):
				return EType(substitute(e, ctx), f).at(expr.pos);
			case EThrow(e):
				return EThrow(substitute(e, ctx)).at(expr.pos);
			case EIn(e1, e2):
				return EIn(substitute(e1, ctx), substitute(e2, ctx)).at(expr.pos);
			case EContinue:
				return expr;
			case EBreak:
				return expr;
			case ECast(e, t):
				return ECast(substitute(e, ctx), t).at(expr.pos);
			case EBlock(exprs):
				return EBlock(substituteBlock(exprs, ctx)).at(expr.pos);
			case EDisplay(e, isCall):
				return EDisplay(substitute(e, ctx), isCall).at(expr.pos);
			case EDisplayNew(t):
				return expr;
			case EArray(e1, e2):
				return EArray(substitute(e1, ctx), substitute(e2, ctx)).at(expr.pos);
			case EFunction(name, f):
				return expr;
			case ETry(e, catches):
				var ret = [];
				for (v in catches) ret.push( { name:v.name, type:v.type, expr:substitute(v.expr, ctx) } );
				return ETry(substitute(e, ctx), ret).at(expr.pos);
			case ESwitch(e, cases, edef):
				var ret = [];
				for (v in cases) ret.push( { values:substituteBlock(v.values, ctx), expr:substitute(v.expr, ctx) } );
				return ESwitch(substitute(e, ctx), ret, edef == null ? null : substitute(edef, ctx)).at(expr.pos);
			default:
				throw "Not implemented: " +expr;
				return expr;
		}
	}
	
	static function substituteBlock(exprs:Array<Expr>, ctx:MonadicContext):Array<Expr>
	{
		var v = [];
		for (i in 0...exprs.length)
			v[i] = substitute(exprs[i], ctx);
		return v;
	}	
	
	static function unify(newType:Option<TypeInfo>, oldType:Option<TypeInfo>, p):TypeInfo
	{
		var typeInfo = switch(newType)
		{
			case None:
				switch(oldType)
				{
					case None:
						Context.error("[Typer] Unification failed, no type found.", p);
					case Some(t):
						t;
				}
			case Some(n):
				switch(oldType)
				{
					case None:
						n;
					case Some(o):
						if (n.name != o.name)
							switch(Resolver.unify(n.name, o.name))
							{
								case Failure(message):
									return Context.error("[Typer] Cannot unify " +n.name + " with " +o.name + ": " +message, p);
								case Success(uni):
									{ name:uni, subtype:n.subtype, neutral:n.neutral };
							}
						else
							n;
				}
		}
		
		switch(Resolver.resolveMonadicClass(typeInfo.name))
		{
			case None:
				Context.error("[Typer] Could not resolve monadic class for " +typeInfo.name, p);
			case Some(type):
		}
		
		return typeInfo;
	}
	
	#end
}